package com.bitsfromspace.ubertorrent.bencoding;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 *
 * Represents a BEncoding value. The java type variant of the value can be asserted by
 * calling {@link #getType()}.
 * <br/>
 *<br/>
 * <b>Bencoding mapping:</b>
 * <br/>
 *
 * <table border=1>
 * <tr>
 * 		<th>Type:</th>
 * 		<th>Value returned by {@link #getType()} :</th>
 * 		<th>Appropriate method to call:</th>
 * </tr>
 * <tr>
 * 		<td>String</td>
 * 		<td> {@link Type#STRING}</td>
 * 		<td> {@link #getString()}</td>
 * </tr>
 * <tr>
 * 		<td>Long</td>
 * 		<td> {@link Type#INTEGER}</td>
 * 		<td> {@link #getInteger()}</td>
 * </tr>
 * <tr>
 * 		<td>List</td>
 * 		<td> {@link Type#LIST}</td>
 * 		<td> {@link #getList()}</td>
 * </tr>
 * <tr>
 * 		<td>Map</td>
 * 		<td> {@link Type#MAP}</td>
 * 		<td> {@link #getMap()}</td>
 * </tr>
 * </table>
 *<br/>
 *Failing to call the correct method for the {@link Type} will result in a {@link ClassCastException}. Method {@link #getValue()} can be called regardless of the {@link Type}.
 *<br/><br/>
 * Utility methods {@link #wrap(Object)} and {@link #unwrap(BValue)} allow quick conversion between BValues and their java equivalent.
 *
 *@author chris.oudeman
 * @since Jan, 4 2011
 *
 */
public class BValue {

	public final static Charset UTF8 = Charset.forName("UTF-8");

	public enum Type {
		STRING, INTEGER, LIST, MAP
	};

	private final Object value;
	private final Type type;

	/**
	 * Creates a new {@link BValue} of type {@link Type#STRING}.
	 */
	public BValue(byte[] data) {

		this(Type.STRING, data);

	}

	/**
	 * Creates a new {@link BValue} of type {@link Type#STRING}.
	 */
	public BValue(String string) {

		this(Type.STRING, string.getBytes(UTF8));

	}

	/**
	 * Creates a new {@link BValue} of type {@link Type#INTEGER}.
	 */

	public BValue(Long l) {

		this(Type.INTEGER, l);
	}

	/**
	 * Creates a new {@link BValue} of type {@link Type#LIST}.
	 */
	public BValue(List<BValue> list) {

		this(Type.LIST, list);
	}

	/**
	 * Creates a new {@link BValue} of type {@link Type#MAP}.
	 */
	public BValue(Map<BValue, BValue> map) {

		this(Type.MAP, map);
	}

	/**
	 * Creates a new {@link BValue} of type <em>type</em>
	 */
	protected BValue(Type type, Object value) {

		this.type = type;
		this.value = value;
	}

	/**
	 * Returns the {@link Type} of this {@link BValue}.
	 * @return the {@link Type} of this {@link BValue}.
	 */
	public Type getType() {

		return type;
	}

	/**
	 * Returns this value as {@link String}. This method will throw
	 * an exception if {@link #getType()} doesn't equal {@link Type#STRING}.
	 *
	 *  @return This value as String
	 *  @throws ClassCastException when {@link #getType()} doesn't equal {@link Type#STRING}.
	 */
	public String getString() {

		return new String((byte[]) value);
	}

	/**
	 * Returns this value as a byte string.
	 */
	public byte[] getByteString() {

		return (byte[]) value;
	}

	/**
	 * Returns this value as {@link Integer}. This method will throw
	 * an exception if {@link #getType()} doesn't equal {@link Type#INTEGER}.
	 *
	 *  @return This value as Integer
	 *  @throws ClassCastException when {@link #getType()} doesn't equal {@link Type#INTEGER}.
	 */
	public Long getInteger() {

		return (Long) value;
	}

	/**
	 * Returns this value as {@link List}. This method will throw
	 * an exception if {@link #getType()} doesn't equal {@link Type#LIST}.
	 *
	 *  @return This value as List
	 *  @throws ClassCastException when {@link #getType()} doesn't equal {@link Type#LIST}.
	 */
	@SuppressWarnings("unchecked")
	public List<BValue> getList() {

		return (List<BValue>) value;
	}


	/**
	 * Returns this value as {@link Map}. This method will throw
	 * an exception if {@link #getType()} doesn't equal {@link Type#MAP}.
	 *
	 *  @return This value as Map
	 *  @throws ClassCastException when {@link #getType()} doesn't equal {@link Type#MAP}.
	 */
	@SuppressWarnings("unchecked")
	public Map<BValue, BValue> getMap() {

		return (Map<BValue, BValue>) value;
	}

	/**
	 * Returns {@link #getValue()} as {@link Map} with all elements unwrapped (see {@link #unwrap(Object)}).
	 * This method will throw an exception if {@link #getType()} doesn't equal {@link Type#LIST}.
	 * 
	 * @see #unwrap(Object)
	 * @see #getMap()
	 */
	public Map<?, ?> getMapUnwrapped() {

		return (Map<?, ?>) unwrap(this);
	}

	/**
	 * Returns this value as {@link List} with elements unwrapped (see {@link #unwrap(Object)}.
	 * This method will throw an exception if {@link #getType()} doesn't equal {@link Type#LIST}.
	 * @see #unwrap(Object)
	 * @see #getList()
	 */
	public List<?> getListUnwrapped() {

		return (List<?>) unwrap(this);
	}

	/**
	 * Returns this value as an {@link Object}. Please use {@link #getType()} to determine
	 * which type to expect and call the approriate <em>getter</em> accordingly.
	 *
	 *  @return This value as Object
	 *  @see #getInteger()
	 *  @see #getList()
	 *  @see #getMap()
	 *  @see #getString()
	 */
	public Object getValue() {

		return value;
	}

	@Override
	public int hashCode() {

		if (getType() == Type.STRING) {
			return getString().hashCode();
		}
		return value.hashCode();
	}

	@Override
	public boolean equals(Object obj) {

		if (obj instanceof BValue){
			BValue that = (BValue) obj;
			if (that.getType() == this.getType()
					&& this.getType() == Type.STRING) {
				return this.getString().equals(that.getString());
			}
			return this.value.equals(that.value);
		}
		return false;
	}


	/**
	 * Returns the value of {@link #getValue()}{@link #toString()}
	 */
	@Override
	public String toString() {

		Object printValue = getType() == Type.STRING
				? getString()
				: value;

		return new StringBuilder("BValue<")
				.append(printValue).append(">").toString();
	}

	/**
	 * Wraps java values to their {@link BValue} equivalent:
	 * <ul>
	 * 	<li>{@link String} will be converted to {@link BValue} of type  {@link Type#STRING} </li>
	 * <li>{@link Integer} and {@link Long} will be converted to {@link BValue} of type  {@link Type#INTEGER} </li>
	 * <li>{@link List} will be converted to {@link BValue} of type  {@link Type#LIST}; Nested elements are converted to their equivalent BValues recursively. </li>
	 * <li>{@link Map} will be converted to {@link BValue} of type  {@link Type#Map};  Nested elements are converted to their equivalent BValues recursively. </li>
	 * <li>Any other types are converted to {@link BValue} of type {@link Type#STRING}. The value is determined by calling {@link Object#toString()} on the passed value.</li>
	 *</ul>
	 */
	public static BValue wrap(Object value) {

		if (value instanceof byte[]) {
			return new BValue((byte[]) value);
		}
		if (value instanceof String) {
			return new BValue(value.toString().getBytes(UTF8));
		}
		if (value instanceof Long || value instanceof Integer) {
			return new BValue(Long.valueOf(value.toString()));
		}
		if (value instanceof List) {
			@SuppressWarnings("rawtypes")
			List<?> sourceList = (List) value;
			List<BValue> destList = new ArrayList<BValue>(sourceList.size());
			for (Object object : sourceList) {
				destList.add(wrap(object));
			}
			return new BValue(destList);
		}
		if (value instanceof Map) {
			Map<?, ?> sourceMap = (Map<?, ?>) value;
			Map<BValue, BValue> destMap =
					new HashMap<BValue, BValue>(sourceMap.size());
			for (Entry<?, ?> entry : sourceMap.entrySet()) {
				destMap.put(wrap(entry.getKey()), wrap(entry.getValue()));
			}
			return new BValue(destMap);
		}
		// last resort: byte string...
		return new BValue(value.toString());
	}

	/**
	 * Unwraps nested {@link BValue} elements to their java equivalent.
	 * <ul>
	 * 	<li> {@link BValue} of type  {@link Type#STRING} are unwrapped to a java {@link String}</li>
	 * <li>{@link BValue} of type  {@link Type#INTEGER} are unwrapped to a java {@link Long}</li>
	 * <li> {@link BValue} of type  {@link Type#LIST} are unwrapped to a {@link List}; Nested elements are also unwrapped to their equivalent java types recursively. </li>
	 * <li>{@link BValue} of type  {@link Type#Map} are unwrapped to a {@link Map};  Nested elements are also unwrapped to their equivalent java types recursively. </li>
	 *</ul>
	 */

	public static Object unwrap(BValue value) {

		if (value instanceof BValue) {
			BValue bValue = value;
			switch (bValue.getType()) {
			case LIST: {
				List<BValue> sourceList = bValue.getList();
				List<Object> destList =
						new ArrayList<Object>(sourceList.size());

				for (BValue nxValue : sourceList) {
					destList.add(unwrap(nxValue));
				}
				return destList;
			}
			case MAP: {
				Map<BValue, BValue> sourceMap = bValue.getMap();
				Map<Object, Object> destMap =
						new HashMap<Object, Object>(sourceMap.size());

				for (Entry<BValue, BValue> entry : sourceMap.entrySet()) {
					destMap.put(
							unwrap(entry.getKey()),
							unwrap(entry.getValue()));
				}
				return destMap;

			}
			case STRING: {
				return bValue.getString();
			}
			default:
				return bValue.getValue();
			}
		}
		return value;

	}

}


